Skip to content

I18n#372

Draft
freekh wants to merge 2 commits intomainfrom
i18n
Draft

I18n#372
freekh wants to merge 2 commits intomainfrom
i18n

Conversation

@freekh
Copy link
Contributor

@freekh freekh commented Oct 17, 2025

No description provided.

@changeset-bot
Copy link

changeset-bot bot commented Oct 17, 2025

⚠️ No Changeset found

Latest commit: a27bcca

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@freekh freekh marked this pull request as draft October 17, 2025 14:34
Comment on lines +23 to +27
nextAppRouter.localize({
moduleName: "locale",
segment: "locale",
translations: "translations",
}),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@isakgb this means that this router is not in page.val.ts, but the module names are the locale. In addtion: the locale segment is "locale" (and must be an actual locale).

"en-us": "English",
"nb-no": "Norwegian",
"translation-is-available-in":
"This blog is also available in the following languages:",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@isakgb This is useful if you need translations that are "by field".

@freekh
Copy link
Contributor Author

freekh commented Nov 2, 2025

We're now thinking that the initial version will support a .locale method on all schemas which will use this locale on all input fields (so browser can provide auto correct).

The UI will have a filter option in the nav menu (we're not sure about this yet), where the current locale be selected. Selecting a locale will filter across the entire selection.

Implementation

All schemas will have a .locale() method. This does nothing other that change the input language in the UI in the fields below, in addition to add a filter to it. The .locale() method takes 1 argument which is a string representing the language tag. Language tags are RFC-ish. We're thinking they should be compatible with 1) urls 2) record keys. That means they probably must be 1) lower-cased 2) use underscore (_) not hypens (-).

Later we think we will want to follow up with the following features.

Translations

Not sure about this yet, but we could add a translation property to locale as well.
.locale("nb_no", { translate: "records-of-same-schema" } )
We need to find a way to do this in a forward compatible manner, but in the example above, all records of the same schema would be translatable.

We could even extend this with a way to store the translations.
.locale("nb_no", { translate: "records-of-same-schema", translations: translationsVal } )

An alternative to the approach above would be to have a "translation key" or some id that indicates that something is translate-able.
.locale("nb_no", { translationKey: "my-pages", translations: translationsVal } )

We need to consider how the "translationsVal" would work as well. For records, it would be logical to create an entry, where the key of a source record maps to a record of target keys by language tag.

{
  my_thing: { "fr_fr": "mon_truc" },
  // maybe we want bidirectional as well?
  mon_truc: { "en_us": "my_thing" }
}

For other types than record, it is less clear what would be practical. We could do source paths, but that means user will not be able to easily understand + means we need to provide a utility to fetch something based on a module path, which, since it will potentially have to load any type of module, breaks a bit with how we usually do things ( because we can't type things).
Note that it feels like a good idea to just constrain translations to be either between records or between fields inside the same object / record.

Yet another way to do this is to do:
.locale("nb_no", { get translate(): { return [otherModuleVal, ] } } )
This would provide more type-safety, but would most likely be less ergonomic?

Field level translations

To enable "field level" translation, we will add the following features:

  • a s.languages() schema which can be seen a short hand of: s.union(s.literal("en_us").language(true /required/), ...). We might actually later add a .language validator on literal / string to make it possible. We need to think about other places this could be (mis)used.
  • a key schema to s.record
const localizedSchema = s.record(s.languages(["en_us", "nb_no"], { optional: ["fr_fr"]})), s.object({ ... }))

We'll follow up with more details about how we handle routers wrt localization. Maybe we should add an optional way to do schemas per segments in a router:

s.record(...).router(nextAppRouter(), { segments: { country: s.languages(["en_us", "fr_fr"]) }})

Appendix

These are the discarded alternatiev representations:

.i18n on object with language tags

const localizedSchema = s.object({
  en_US: s.string(),
  fr_FR: s.string().nullable(),
}).i18n()

This will be means that each of the properties maps to a locale. The keys must be valid language tags.

Alternative object representation

We considered something like this:

const localizedSchema = s.object.i18n(s.string(), ["en_US"], ["fr_FR"])

But decided against it, since we want to make it easy to discern the shape of Val data.

@freekh freekh force-pushed the main branch 2 times, most recently from 272f3ac to 8d6daf7 Compare January 21, 2026 05:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant